<?php
###########################################################################
#  Copyright (C) 2006-2007 Glyphix, Inc. <briz@glyphix.com>               #
#                                                                         #
#  Permission is hereby granted, free of charge, to any person obtaining  #
#  a copy of this software and associated documentation files (the        #
#  "Software"), to deal in the Software without restriction, including    #
#  without limitation the rights to use, copy, modify, merge, publish,    #
#  distribute, sublicense, and/or sell copies of the Software, and to     #
#  permit persons to whom the Software is furnished to do so, subject to  #
#  the following conditions:                                              #
#                                                                         #
#  The above copyright notice and this permission notice shall be         #
#  included in all copies or substantial portions of the Software.        #
#                                                                         #
#  Except as contained in this notice, the name(s) of the above           #
#  copyright holders shall not be used in advertising or otherwise to     #
#  promote the sale, use or other dealings in this Software without       #
#  prior written authorization.                                           #
#                                                                         #
#  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,        #
#  EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF     #
#  MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. #
#  IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR      #
#  OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,  #
#  ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR  #
#  OTHER DEALINGS IN THE SOFTWARE.                                        #
###########################################################################

/**
 * GXDB, a simple GXPage module for PDO.
 * It provides two methods -- set and get -- and handles all the additional work (such as binding variables) necessary when working with PDO.
 * @author		Brad Brizendine <briz@glyphix.com>
 * @link		http://www.glyphix.com/
 * @license		http://opensource.org/licenses/bsd-license.php BSD License
 * @version		1.0
 * @package		GXPage
 * @subpackage	Modules
 * @copyright	Copyright 2006 - 2007, Glyphix, Inc.
 * @uses		GXModule
 *
 * When GXPage instantiates it, GXPage passes it the XML config from System.xml.
 * If it receives a GXDOM object as its config, it will use the first connection it finds.
 * Once instantiated, you can create a new connection by passing it the name of a connection, as defined in System.xml.
 * GXModule caches each connection, and you can obtain access to them as shown in the example.
 *
 * @example get a connection from GXModule
 * // Assuming your class has defined GXPage as a class var:
 * // Database is the node name of the module definition in System.xml
 * // System is the node name of the connection definition in System.xml
 * $this->GXPage->Database->System
 *
 * @example opening a new connection
 * // Assuming there is a connection definition in System.xml called "Dev":
 * $devdb = new GXDB('Dev');
 *
 * @param mixed $config GXDOM config or name of a connection defined in the <Database> section of System.xml
 * @return mixed itself or false on failure
 */
class GXDB extends GXModule {

	// instructs GXDB::set to update instead of insert
	// use: GXDB::SQL_UPDATE
	const SQL_UPDATE	= 'SQL_UPDATE';
	const NO_FETCH		= 'NO_FETCH';
	const FETCH_ASSOC	= PDO::FETCH_ASSOC;
	const FETCH_OBJ		= PDO::FETCH_OBJ;
	const FETCH_BOTH	= PDO::FETCH_BOTH;

	// named connections
	private $drivers = array();

	public function __construct( $config = null ){
		parent::__construct();

		$name = null;
		switch(true){
			// if config is a string, treat it as a connection name
			case is_string($config):
				$name = $config;
				// get the named connection
				$xpath = '//Modules/Database/*[ name() = "' .$name .'" ]';
				$config = $this->SystemConfig->xpath($xpath,0);
				break;
			// if it's an xml object, treat it as the config from SystemConfig
			case get_class($config) == 'GXDOM':
				// get the first connection
				$xpath = '*[1]';
				$config = $config->exec->xpath($xpath,0);
				$name = $config->nodeName;
				break;
			// otherwise, we have an error
			default:
				trigger_error( 'Unable to parse config in GXDB constructor (' .gettype($config) .')', E_USER_ERROR );
				return false;
		}

		// instantiate the db subclass
		$type = strval($config->exec->xpath('Type',0)->nodeValue);
		$host = strval($config->exec->xpath('Host',0)->nodeValue);
		$database = strval($config->exec->xpath('Database',0)->nodeValue);

		// store the connect string
		$dsn = $type .':host=' .$host .';dbname=' .$database;
		// attempt to connect, and capture the error if we can't
		try{
			$db = new PDO( $dsn, strval($config->exec->xpath('User',0)->nodeValue), strval($config->exec->xpath('Password',0)->nodeValue) );
			$db->setAttribute( PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION );
			// set timezone if we're running postgresql and have a timezone setting
			if($type == 'pgsql'){
				// set the timezone if we have one
				if( $tz = $this->GXPage->Run->Timezone ){
					if( $db->exec( 'SET TIME ZONE \'' .$tz .'\'' ) ){
						trigger_error( 'Set database timezone to ' .$tz, E_USER_WARNING );
					}
				}
			}
			// set the character set
			if($type == 'mysql'){
				$db->exec('SET CHARACTER SET utf8');
			}
		}catch( PDOException $e ){
			trigger_error( 'Unable to make a connection: ' .$e->getMessage() .'::' .print_r($e,true), E_USER_ERROR );
			return false;
		}

		trigger_error( 'GXDB connected to ' . $type .'::' .$database .' using config named ' .$name, E_USER_WARNING );

		// return the database-specific driver
		// don't use GXObject so we can have as many of them as we like
		$d = 'GXDB_' .$type;
		if( !($driver = new $d($db)) ){
			trigger_error( 'Unable to load driver ' .$d, E_USER_ERROR );
			return false;
		}

		// cache this driver
		$this->drivers[ $name ] = $driver;
		return $this;
	}

	/**
	 * __get
	 *
	 * Returns the GXDB_* connection by the name specified in SystemConfig.
	 * @param string $name the name of the connection defined in System.xml
	 * @return mixed the connection driver or false on failue
	 */
	function __get( $name = null ){
		if( isset($this->drivers[$name]) ){
			return $this->drivers[ $name ];
		}
		trigger_error( 'No connection by that name (' .$name .')', E_USER_WARNING );
		return false;
	}

}

/**
 * GXDB_driver, driver management for GXDB.
 * This class is only used by GXDB.
 * @author		Brad Brizendine <briz@glyphix.com>
 * @link		http://www.glyphix.com/
 * @license		http://opensource.org/licenses/bsd-license.php BSD License
 * @version		1.0
 * @package		GXPage
 * @subpackage	GXDB
 * @copyright	Copyright 2006 - 2007, Glyphix, Inc.
 *
 * @param object $PDO the PDO connection object as established by GXDB
 * @return mixed itself or false on failure
 */
class GXDB_driver {

	// PDO connection object, must be passed to this class' constructor
	private $PDO;

	// holds table definitions created in describe()
	protected $Tables;

	/**
	 * __construct
	 *
	 * Sets the PDO connection.
	 */
	public function __construct( &$PDO = null ){
		if( !is_object($PDO) ){
			trigger_error( 'Missing database connection', E_USER_ERROR );
			return false;
		}
		$this->PDO = $PDO;
		return $this;
	}

	/**
	 * __get
	 *
	 * This is a stub for future functionality.
	 * @param string $name the name of the property
	 * @return mixed whatever the property is
	 */
	function __get( $name = null ){
		return $this->$name;
	}

	/**
	 * describe
	 *
	 * This function is extended by each database driver (see GXDB_psql for an example).
	 * When the database driver has completed its work, it calls its parent (this), providing the required params.
	 * @param string $table the name of the table
	 * @param array $cols the SQL definition of the table as an array
	 * @return array the table definition or false on failure
	 */
	public function describe( $table = null, $cols = null ){
		if( !$table || !$cols ){
			trigger_error( 'Missing table or cols', E_USER_ERROR );
			return false;
		}

		// parse the definition, grouping columns by function
		$def['schema'] = $cols;

		// list primary and foreign columns
		// use the same value for the key and the value so we can use keys as well as in_array()
		foreach( $cols as $c => $conf ){
			if( $conf['primary'] ){
				$def['primary'][$c] = $c;
			}
			if( $conf['foreign'] ){
				$def['foreign'][$c] = $c;
			}
		}

		// cache the table definition
		$this->Tables[ $table ] = $def;
		return $def;
	}

	/**
	 * pdopararms
	 *
	 * Preps the col=>val array passed into the get and set functions.
	 * @param array $params col=>val associative array
	 * @return array of false on failure
	 */
	public function pdoparams( $params = null ){
		if( !is_array($params) || !count($params) ){
			trigger_error( 'No params', E_USER_WARNING );
			// return zero-length array
			return array();
		}
		// massage the array: add a colon to the beginning of each key and make sure the value is valid
		$slashes = get_magic_quotes_gpc();
		// if the first key already has a colon, just return the array; it's already been handled
		if( substr(key($params),0,1) != ':' ){
			foreach( $params as $k => $v ){
				$params[ ':' .$k ] = ($slashes) ? stripslashes($v) : $v;
				unset($params[$k]);
			}
		}
		return $params;
	}

	/**
	 * get
	 *
	 * Prepares the supplied sql, executes it and fetches its results.
	 * It accepts a variable number of parameters as long as the required ones come in order.
	 * Examples:
	 *
	 * @param string $sql the query
	 * @param array $params associative array of parameters for the execute statement
	 * @param constant $fetching constant must be a valid PDO:: constant such as PDO::FETCH_OBJ
	 * @param boolean $fetch when true (default), performs a fetchAll; when false, returns the number of affected rows
	 * @return mixed whatever @fetching specifies or false on failure
	 */
	public function get(){
		// set up the args we'll use
		$sql		= null;				// the sql string
		$params		= array();			// associative array of parameters
		$fetching	= PDO::FETCH_OBJ;	// pdo fetch style
		$nofetch	= false;			// whether to fetch or to just return rowCount
		$limit		= null;				// number of records to return (if not set, get() returns fetchAll())
		$offset		= null;				// record number to start the fetch

		// sense constants
		$args = func_get_args();
		$argmap = array(
			'sql',
			'params',
			'limit',
			'offset'
			);
		// loop thru all args, pulling the arg's name from the top of the argmap stack
		// if the arg equals one of this function's supported constants, don't touch the argmap
		foreach( $args as $a ){
			switch($a){
				case PDO::FETCH_ASSOC:
				case PDO::FETCH_OBJ:
				case PDO::FETCH_BOTH:
					$fetching = $a;
					continue;
					break;
				case GXDB::NO_FETCH:
					$nofetch = true;
					break;
				default:
					$name = array_shift($argmap);
					${$name} = $a;
					break;
			}
		}

		// sanity check
		if( !$sql ){
			trigger_error( 'Missing sql', E_USER_ERROR );
			return false;
		}
		if( !$offset ){
			$offset = 0;
		}
		// massage the params array
		$params = $this->pdoparams($params);

		// init the query var
		$q = null;
		trigger_error( 'GXDB get operation: ' .$sql .' with ' .print_r($params,true), E_USER_WARNING );

		// if we have limit, start a transaction or pgsql won't create a proper scroll cursor
		/*
		if( is_numeric($limit) ){
			trigger_error( 'pgsql ... beginTransaction', E_USER_WARNING );
			$this->PDO->beginTransaction();
		}
		*/

		// prepare the query
		try{
			// if we have limit, prepare a scrolling cursor
			$scroll = ( is_numeric($limit) ? array( PDO::ATTR_CURSOR => PDO::CURSOR_FWDONLY ) : array() );
			$q = $this->PDO->prepare($sql,$scroll);
		}catch( PDOException $e ){
			trigger_error( 'PDO prepare error on ' .$sql .' with ' .print_r($params,true) .': ' .$e->getMessage(), E_USER_ERROR );
			return false;
		}

		// run it
		try{
			$q->execute($params);
		}catch( PDOException $e ){
			trigger_error( 'PDO error executing query: ' .$sql .' with ' .print_r($params,true) .': ' .$e->getMessage(), E_USER_ERROR );
			return false;
		}

		// if we're not fetching, just return rowcount, since q::execute already ran the sql
		if( $nofetch ){
			$numaffected = $q->rowCount();
			unset($q);
			trigger_error( 'NOFETCH specified: ' .$numaffected .' (' .gettype($numaffected) .') rows affected', E_USER_ERROR );
			return $numaffected;
		}

		// init the return var
		$r = null;

		// get the results using fetchAll, since there's a bug in pdo which won't allow scrolling cursors for pgsql
		try{
			$data = $q->fetchAll($fetching);
		}catch( PDOException $e ){
			trigger_error( 'PDO fetchAll() error on ' .$sql .' with ' .print_r($params,true) .': ' .$e->getMessage(), E_USER_ERROR );
			return false;
		}

		// if we don't have a limit, just return the result
		if( !is_numeric($limit) ){
			unset($q);
			return $data;
		}

		// if we're here, we have a limit and should therefore build an object that contains the results
		$r = new stdClass();
		$r->rowCount = $q->rowCount();
		// loop thru the results starting at offset, offset plus limit times
		$r->data = null;
		for( $i = $offset; $i < ($offset + $limit); $i++ ){
			if( $i >= $r->rowCount ){
				break;
			}
			$r->data[] = $data[$i];
		}
		//$this->PDO->commit();
		unset($q);
		return $r;

		/* scrolling cursor code; does not work in current php/pdo/pgsql
		// get the results
		$r = null;
		if( !is_numeric($limit) ){
			// fetch all rows
		}else{
			// fetch specified rows
			// set the return class
			$r = new stdClass();
			// the total number of results
			$r->rowCount = $q->rowCount();
			// the rows as a numerically-indexed array
			$r->data = null;
			try{
				// loop thru the results starting at offset, offset plus limit times
				for( $i = $offset; $i < ($offset + $limit); $i++ ){
					if( $i >= $r->rowCount ){
						break;
					}
					$r->data[] = $q->fetch( $fetching, PDO::FETCH_ORI_ABS, $i );
				}
				// must close the cursor, or we'll have database problems
				$q->closeCursor();
			}catch( PDOException $e ){
				trigger_error( 'PDO fetch() error on ' .$sql .' with ' .print_r($params,true) .': ' .$e->getMessage(), E_USER_ERROR );
				return false;
			}
		}
		if( is_numeric($limit) ){
			$this->PDO->commit();
		}
		// free PDO
		unset($q);
		return $r;
		*/
	}

	/**
	 * run
	 *
	 * Returns this::get, requesting that no fetch be performed by this::get.
	 * This is a good shortcut for UPDATE and DELETE operations.
	 * @param string $sql see definition in this::get
	 * @param array $params see definition in this::get
	 * @return see definition in this::get
	 */
	public function run( $sql = null, $params = null ){
		// return get, but tell it not to execute a fetch
		return $this->get( $sql, $params, PDO::FETCH_OBJ, GXDB::NO_FETCH );
	}

	/**
	 * set
	 *
	 * Performs an insert or update.
	 *
	 * @param string $table the name of the table
	 * @param array $params an associative array of name=>value pairs (not recursive) ... each key must match a column name exactly
	 * @param contant $operation instructs set to attempt an update if the value is GXPAGE::SQL_UPDATE
	 * @return mixed the last insert id or false on failure
	 */
	public function set( $table = null, $params = null, $operation = null ){
		if( !$table || !is_array($params) ){
			trigger_error( 'Missing table or params', E_USER_ERROR );
			return false;
		}

		// init vars
		$slashes		= get_magic_quotes_gpc();	// determine the magic quotes setting
		$def			= null;						// the table definition
		$info			= array();					// full definition of each column in associative format by column name
		$cols['names']	= array();					// the unaffected names of the columns derived from params
		$cols['params']	= array();					// the col names preceded with a colon
		$cols['values']	= array();					// the values of each column
		$cols['update']	= array();					// colname=:colname (update format)
		$where			= array();					// array of columns to be used during an update

		// get the table definition ... the describe method will be overloaded when extended by the driver
		if( !($def = $this->describe($table)) ){
			trigger_error( 'Unable to describe ' .$table, E_USER_ERROR );
			return false;
		}

		// if we have a value for the first unique id and we're in pgsql, add a "returning" clause
		$returning		= null;
		// generate list of columns which define a unique record
		$primarycols	= $def['primary'];
		if( $this->PDO->getAttribute(PDO::ATTR_DRIVER_NAME) == 'pgsql' ){
			if( count($primarycols) ){
				$returning = ' returning ' .implode(',',$primarycols);
			}
		}

		// generate the sql
		$i = 0;
		if( is_array($params) ){
			foreach( $params as $col => $val ){
				// make sure the database has the specified column
				if( !isset($def['schema'][$col]) ){
					trigger_error( 'Table ' .$table .' has no column by that name (' .$col .')', E_USER_WARNING );
					unset($params[$col]);
					continue;
				}

				// prefix colname for pdo
				$prm = ':' .$col;
				$val = $slashes ? stripslashes($val) : $val;
				// if we have a boolean field, make sure pgsql gets the appropriate boolean format
				if( $this->PDO->getAttribute(PDO::ATTR_DRIVER_NAME) == 'pgsql' && $def['schema'][$col]['type'] == 'boolean' ){
					$val = ($val || strtolower($val) == 't') ? 't' : 'f';
					// change the source params also
					$params[$col] = $val;
				}

				// build the overall array with easy access to all the variations for one column
				$info[ $col ] = array(
					'name'		=> $col,
					'param'		=> ':' .$col,
					'value'		=> $val,
					'update'	=> $col .'=' .$prm,
					'index'		=> $i
					);

				// separate non-associative arrays for easy implode()
				$cols['names'][]	= $col;
				$cols['params'][]	= $prm;
				$cols['values'][]	= $val;
				$cols['update'][]	= $col .'=' .$prm;

				// increment
				$i++;
			}
		}

		// default to insert
		$sql = 'insert into ' .$table .' (' .implode(',',$cols['names']) .') values (' .implode(',',$cols['params']) .')' .$returning;

		// if we're updating, get the unique col(s) and override the sql
		if( $operation === GXDB::SQL_UPDATE ){
			// search for unique columns that are also in the provided params
			$wherecols = @array_intersect($cols['names'],$primarycols);
			// if we have no columns to update
			if( !count($wherecols) ){
				trigger_error( 'Update specified, but there are no columns to update (' .print_r($cols['names'],true) .'::' .print_r($primarycols,true), E_USER_ERROR );
				return false;
			}
			// if we don't have enough values to fill all the primary cols, warn but continue
			if( count($primarycols) > $wherecols ){
				trigger_error( 'The primary key for this table (' .$table .') requires ' .count($primarycols) .' columns but there are only ' .count($wherecols) .' values provided; this update may affect more than one record', E_USER_WARNING );
			}
			foreach( $wherecols as $col ){
				if( isset($info[$col]) ){
					// pdo colname ... need to create a new param name, since this param is already sending data
					$prm = ':' .$col .'_uid';
					// build the WHERE query string
					$where[] = $col .'=' .$prm;
					// add the new param to the pdo params values
					$params[ $col .'_uid' ] = $info[$col]['value'];
					// get rid of this column from all params
					// PDO will fail if we provide columns that are not present in the sql string
					$index = $info[$col]['index'];
					unset($params[$col]);
					unset($cols['names'][$index]);
					unset($cols['params'][$index]);
					unset($cols['values'][$index]);
					unset($cols['update'][$index]);
					unset($info[$col]);
				}
			}

			// check to see we have something in where
			if( !count($where) ){
				trigger_error( 'GXDB::SQL_UPDATE specified, but we have no unique columns to search by', E_USER_ERROR );
				return false;
			}

			// remove columns that cannot be null for which we have no value
			$udpate = null;
			foreach( $cols['update'] as $i => $val ){
				$colname = $cols['names'][$i];
				$nullable = $def['schema'][ $colname ]['null'];
				// if we have a value for this column, include it
				if( strlen($params[$colname]) ){
					$update[] = $val;
					continue;
				}
				// if we have no value and the column cannot be null, skip if
				if( !strlen($params[$colname]) && !$nullable ){
					continue;
				}
				// if we have no value and the column CAN be null, set the value to 'null'
				if( !strlen($params[$colname]) && $nullable ){
					$params[$colname] = null;
					$update[] = $val;
				}
			}

			// create the update sql
			$sql = 'update ' .$table .' set ' .implode(',',$update) .' where ' .implode(' and ',$where) .$returning;
			// prepare the query
			try{
				$q = $this->PDO->prepare($sql);
			}catch( PDOException $e ){
				trigger_error( 'PDO prepare error on ' .$sql .' with ' .print_r($params,true) .': ' .$e->getMessage(), E_USER_ERROR );
				return false;
			}

			// run it
			try{
				// prep the query
				$this->prepQuery($q, $params, $def);
				// run the query
				$q->execute();
			}catch( PDOException $e ){
				$err = array();
				foreach($params as $key => $val){
					$err[$key] = substr($val, 0, 100);
				}
				trigger_error( 'PDO error executing query: ' .$sql .' [' .$e->getMessage() .']', E_USER_ERROR );
				trigger_error( 'PDO error execute params: ' .print_r($err,true), E_USER_ERROR);
				return false;
			}

			// return the num rows affected
			$numaffected = $q->rowCount();
			// memory cleanup
			unset($q);
			trigger_error( 'SQL_UPDATE specified: ' .$numaffected .' (' .gettype($numaffected) .') rows affected', E_USER_WARNING );
			return $numaffected;
		}

		// if we're pgsql and we have returning, issue a fetch
		if( $returning ){
			trigger_error( '\'Returning\' in an insert operation specified; calling get', E_USER_WARNING );
			return $this->get($sql,$params);
		}
		trigger_error( 'GXDB set operation: ' .$sql .' with ' .print_r($params,true), E_USER_WARNING );

		// otherwise, prepare and execute
		try{
			$q = $this->PDO->prepare($sql);
		}catch( PDOException $e ){
			trigger_error( 'PDO prepare error on ' .$sql .' with ' .print_r($params,true) .': ' .$e->getMessage(), E_USER_ERROR );
			return false;
		}

		// prep the query
		$this->prepQuery($q, $params, $def);

		// execute the statement
		try{
			$q->execute();
		}catch( PDOException $e ){
			trigger_error( 'PDO execute error on ' .$sql .' with ' .print_r($params,true) .': ' .$e->getMessage(), E_USER_ERROR );
			return false;
		}

		// get the last id inserted
		$lastid = $this->PDO->lastInsertId();
		// if we don't have one (the table might not have an auto-increment field), make it true
		if( !$lastid ){
			$lastid = true;
		}
		if(is_bool($lastid)){
			trigger_error('New record for ' .$table .' created, but there is no auto-increment field so no new id', E_USER_WARNING);
		}else{
			trigger_error('New record for ' .$table .' created: [' .$lastid .']', E_USER_WARNING);
		}
		return $lastid;
	}

	/**
	 * prepQuery
	 *
	 * Pass in the pdo statement object (after prepare() on the sql),
	 * the params (without the beginning colon) and the table def.
	 */
	private function prepQuery( &$q, $params = null, $def = null ){
		if( !is_object($q) ){
			trigger_error( 'Object is not a valid PDO statement object', E_USER_ERROR );
			return false;
		}
		// attempt to convert each param according to the expected data type
		// pdo types are PDO::PARAM_BOOL, PDO::PARAM_INT, PDO::PARAM_STR, PDO::PARAM_NULL
		foreach( $params as $key => $val ){
			// look for the key in schema, fall back to params (_uid params are stored in params, not def)
			$col = isset($def['schema'][$key]) ? $def['schema'][$key]['phptype'] : gettype($val);
			$key = is_numeric(strpos($key,':')) ? $key : ':' .$key;
			switch($col['phptype']){
				case 'string':
					// if the database column type is boolean and we have pgsql t/f value
					if( $col['type'] == 'bool' && ($val == 't' || $val == 'f') ){
						if( !($q->bindValue( $key, $val, PDO::PARAM_BOOL )) ){
							trigger_error( 'Unable to bind ' .$val .' to ' .$key .' as PDO::PARAM_BOOL', E_USER_ERROR );
						}
					}else{
						if( !($q->bindValue( $key, $val, PDO::PARAM_STR )) ){
							trigger_error( 'Unable to bind ' .$val .' to ' .$key .' as PDO::PARAM_STR', E_USER_ERROR );
						}
					}
					break;
				case 'numeric':
					// if the database type is text or character, phptype might still report 'numeric'
					if( $col['type'] == 'text' || is_numeric(strpos($col['type'],'character')) ){
						if( !($q->bindValue( $key, $val, PDO::PARAM_STR )) ){
							trigger_error( 'Unable to bind ' .$val .' to ' .$key .' as PDO::PARAM_STR', E_USER_ERROR );
						}
					// if we have a true integer, bind it as such
					}elseif( gettype($val) == 'integer' ){
						if( !($q->bindValue( $key, $val, PDO::PARAM_INT )) ){
							trigger_error( 'Unable to bind ' .$val .' to ' .$key .' as PDO::PARAM_INT', E_USER_ERROR );
						}
					// PDO doesn't offer binding as a float
					}else{
						if( !($q->bindValue( $key, $val )) ){
							trigger_error( 'Unable to bind ' .$val .' to ' .$key .' (not using a PDO type constant)', E_USER_ERROR );
						}
					}
					break;
				default:
					if( !($q->bindValue( $key, $val )) ){
						trigger_error( 'Unable to bind ' .$val .' to ' .$key .' (not using a PDO type constant)', E_USER_ERROR );
					}
			}
		}
		// the q object now has values bound
		return $q;
	}

}

/**
 * GXDB_pgsql, GXDB driver for PostgreSQL.
 * This class is only used by GXDB.
 * @author		Brad Brizendine <briz@glyphix.com>
 * @link		http://www.glyphix.com/
 * @license		http://opensource.org/licenses/bsd-license.php BSD License
 * @version		1.0
 * @package		GXPage
 * @subpackage	GXDB
 * @copyright	Copyright 2006 - 2007, Glyphix, Inc.
 *
 * @param object $PDO the PDO connection object as established by GXDB
 * @return mixed itself or false on failure
 */
class GXDB_pgsql extends GXDB_driver {

	public function __construct( &$PDO = null ){
		parent::__construct($PDO);
		return $this;
	}

	/**
	 * describe
	 *
	 * Builds an array from the SQL definition of the table name.
	 * @param string $table the name of the table
	 * @param array $def included here to suppress E_STRICT warnings ... see GXDB::describe()
	 * @return array table definition or false on failure
	 */
	public function describe( $table = null, $def = null ){
		if( !$table ){
			trigger_error( 'Missing table', E_USER_ERROR );
			return false;
		}
		// check for cached data
		if( isset($this->Tables[ $table ]) ){
			return $this->Tables[ $table ];
		}

		// build the table info query
		$sql = 'SELECT DISTINCT '
				.'col.attnum AS "order",'
				.'col.attname AS name,'
				.'format_type(col.atttypid,col.atttypmod) AS "type",'
				.'NOT col.attnotnull AS isnullable,'
				.'col.atthasdef AS hasdefault,'
				.'(SELECT substring(pg_attrdef.adsrc for 128) FROM pg_attrdef WHERE pg_attrdef.adrelid = col.attrelid AND pg_attrdef.adnum = col.attnum AND col.atthasdef) AS defaultvalue,'
				.'pcon.conname IS NOT NULL AS isprimary,'
				.'fcon.conname IS NOT NULL AS isforeign '
			.'FROM '
				.'pg_class AS tab JOIN pg_attribute AS col ON (tab.oid = col.attrelid AND col.attnum > 0 AND NOT col.attisdropped) '
				.'JOIN pg_type AS coltype ON (col.atttypid = coltype.oid) '
				.'LEFT JOIN pg_constraint AS pcon ON (pcon.conrelid = tab.oid AND col.attnum = ANY (pcon.conkey) AND pcon.contype = \'p\') '
				.'LEFT JOIN pg_constraint AS fcon ON (fcon.conrelid = tab.oid AND col.attnum = ANY (fcon.conkey) AND fcon.contype = \'f\') '
			.'where '
				.'lower(tab.relname) = :table '
			.'order by '
				.'"order"';

		/*
		 order |   name    |           type           | isnullable | hasdefault |          defaultvalue           | isprimary | isforeign
		-------+-----------+--------------------------+------------+------------+---------------------------------+-----------+-----------
			 1 | id        | bigint                   | f          | t          | nextval('seq_log_id'::regclass) | t         | f
			 2 | action    | character varying        | t          | f          |                                 | f         | f
			 3 | uri       | character varying        | t          | f          |                                 | f         | f
			 4 | referrer  | character varying        | t          | f          |                                 | f         | f
			 5 | useragent | character varying        | t          | f          |                                 | f         | f
			 6 | memberid  | bigint                   | f          | t          | 0                               | f         | t
			 7 | serviceid | bigint                   | f          | t          | 0                               | f         | t
			 8 | menuid    | bigint                   | t          | f          |                                 | f         | t
			 9 | requested | timestamp with time zone | f          | t          | now()                           | f         | f
		*/

		// the initial table definition
		if( !($def = $this->get( $sql, array('table'=>$table), PDO::FETCH_ASSOC )) ){
			trigger_error( 'Unable to describe table ' .$table, E_USER_ERROR );
			return false;
		}

		// the gxpage table definition
		$tabledef = null;

		// build the table definition
		foreach( $def as $conf ){

			// split on paren, which leaves the field length as index 1
			$len = preg_split('/[()]/',$conf['type']);

			// php type
			$phptype = null;
			switch(true){
				case is_numeric(strpos($conf['type'],'char')):
					$phptype = 'string';
					break;
				case is_numeric(strpos($conf['type'],'timestamp')):
					$phptype = 'timestamp';
					break;
				case is_numeric(strpos($conf['type'],'date')):
					$phptype = 'date';
					break;
				case is_numeric(strpos($conf['type'],'bit')):
					$phptype = 'string';
					break;
				default:
					$phptype = 'numeric';
			}

			$tabledef[ $conf['name'] ] = array(
				'type'		=> $conf['type'],
				'length'	=> (isset($len[1])) ? $len[1] : null,
				'null'		=> $conf['isnullable'],
				'default'	=> $conf['defaultvalue'],
				'primary'	=> ($conf['isprimary'] == 't') ? true : false,
				'foreign'	=> ($conf['isforeign'] == 't') ? true : false,
				'phptype'	=> $phptype
				);
		}

		return parent::describe($table,$tabledef);
	}

}

/**
 * GXDB_mysql, GXDB driver for MySQL.
 * This class is only used by GXDB.
 * @author		Brad Brizendine <briz@glyphix.com>
 * @link		http://www.glyphix.com/
 * @license		http://opensource.org/licenses/bsd-license.php BSD License
 * @version		1.0
 * @package		GXPage
 * @subpackage	GXDB
 * @copyright	Copyright 2006 - 2007, Glyphix, Inc.
 *
 * @param object $PDO the PDO connection object as established by GXDB
 * @return mixed itself or false on failure
 */
class GXDB_mysql extends GXDB_driver {

	public function __construct( &$PDO = null ){
		parent::__construct($PDO);
		return $this;
	}

	/**
	 * describe
	 *
	 * Builds an array from the SQL definition of the table name.
	 * @param string $table the name of the table
	 * @param array $def included here to suppress E_STRICT warnings ... see GXDB::describe()
	 * @return array table definition or false on failure
	 */
	public function describe( $table = null, $def = null ){
		if( !$table ){
			trigger_error( 'Missing table', E_USER_ERROR );
			return false;
		}

		/*
		+-----------+---------------+------+-----+-------------------+-----------------------------+
		| Field     | Type          | Null | Key | Default           | Extra                       |
		+-----------+---------------+------+-----+-------------------+-----------------------------+
		| id        | bigint(20)    | NO   | PRI | NULL              | auto_increment              |
		| action    | varchar(1000) | YES  |     | NULL              |                             |
		| uri       | varchar(1000) | YES  |     | NULL              |                             |
		| referrer  | varchar(1000) | YES  |     | NULL              |                             |
		| useragent | varchar(1000) | YES  |     | NULL              |                             |
		| memberid  | bigint(20)    | NO   | MUL | 0                 |                             |
		| serviceid | bigint(20)    | NO   | MUL | 0                 |                             |
		| menuid    | bigint(20)    | YES  | MUL | NULL              |                             |
		| requested | timestamp     | NO   |     | CURRENT_TIMESTAMP | on update CURRENT_TIMESTAMP |
		+-----------+---------------+------+-----+-------------------+-----------------------------+
		*/

		// get the definition
		if( !($def = $this->get('describe ' .$table, PDO::FETCH_ASSOC)) ){
			trigger_error( 'Unable to describe ' .$table, E_USER_ERROR );
			return false;
		}

		// the gxpage table definition
		$tabledef = null;

		// build the table definition
		foreach($def as $conf){

			// split on paren, which leaves the field length as index 1
			$len = preg_split('/[()]/',$conf['Type']);

			// php type
			$phptype = null;
			switch(true){
				case is_numeric(strpos($conf['Type'],'char')):
					$phptype = 'string';
					break;
				case is_numeric(strpos($conf['Type'],'timestamp')):
					$phptype = 'timestamp';
					break;
				case is_numeric(strpos($conf['Type'],'date')):
					$phptype = 'date';
					break;
				case is_numeric(strpos($conf['Type'],'bit')):
					$phptype = 'string';
					break;
				default:
					$phptype = 'numeric';
			}

			$tabledef[ $conf['Field'] ] = array(
				'type'		=> $conf['Type'],
				'length'	=> $len,
				'null'		=> $conf['Null'],
				'default'	=> $conf['Default'],
				'primary'	=> ($conf['Key'] == 'PRI') ? true : false,
				'foreign'	=> ($conf['Key'] == 'MUL') ? true : false,
				'phptype'	=> $phptype
				);
		}

		return parent::describe($table,$tabledef);
	}

}

/**
 * GXDB_mssql, GXDB driver for Microsoft SQL.
 * This class is only used by GXDB.
 * @author		Brad Brizendine <briz@glyphix.com>
 * @link		http://www.glyphix.com/
 * @license		http://opensource.org/licenses/bsd-license.php BSD License
 * @version		1.0
 * @package		GXPage
 * @subpackage	GXDB
 * @copyright	Copyright 2006 - 2007, Glyphix, Inc.
 *
 * @param object $PDO the PDO connection object as established by GXDB
 * @return mixed itself or false on failure
 */
class GXDB_mssql extends GXDB_driver {

	public function __construct( &$PDO = null ){
		parent::__construct($PDO);
		return $this;
	}

	/**
	 * describe
	 *
	 * Builds an array from the SQL definition of the table name.
	 * @param string $table the name of the table
	 * @param array $def included here to suppress E_STRICT warnings ... see GXDB::describe()
	 * @return array table definition or false on failure
	 */
	public function describe( $table = null, $def = null ){
		trigger_error( 'GXDB_mssql is not implemented yet', E_USER_WARNING );
		return false;
	}

}

?>